package com.acargil.sudoku.model.structure;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.acargil.sudoku.model.algorithm.rule.IRule;
import com.acargil.sudoku.model.exception.InvalidCellValueException;
import com.acargil.sudoku.model.exception.InvalidCoordinateException;
import com.acargil.sudoku.model.exception.SudokuValidationException;
import com.acargil.sudoku.model.structure.memento.IMemento;

public class StandardGrid implements IMemento<StandardGrid.Memento>, Serializable{

	private static final long serialVersionUID = 1L;
	
	public static final int SQUARE_SIZE = 3;
	public static final int ROW_NUMBER = 9;
	public static final int COLUMN_NUMBER = 9;
	public static final int NUMBER_OF_CELLS = 81;
	
	private StandardCell[][] cells;
	private transient IRule[] rules;
	private int numberOfFullCells;
	
	public StandardGrid() {
		this.numberOfFullCells = 0;
		this.cells = initCells();
		this.rules = new IRule[2];
		this.rules[0] = new LineRule();
		this.rules[1] = new SquareRule();
	}
	
	private StandardCell[][] initCells() {
		StandardCell[][] newCells = new StandardCell[ROW_NUMBER][COLUMN_NUMBER];
		for (int y = 0; y < ROW_NUMBER; y++) {
			for (int x = 0; x < COLUMN_NUMBER; x++) {
				try {
					newCells[x][y] = new StandardCell(new Coordinate(x, y));
				} catch (InvalidCellValueException e) {
					// it is not possible to catch this exception
				}
			}
		}
		
		return newCells;
	}

	public int getNumberOfFullCells() {
		return this.numberOfFullCells;
	}
	
	public void clearCellValue(int x, int y) {
		try {
			this.cells[x][y].setValue(StandardCell.EMPTY_VALUE);
		} catch (InvalidCellValueException e) {  }
		
		this.numberOfFullCells--;
	}

	public void clearAllCell() {
		for (int y = 0; y < ROW_NUMBER; y++) {
			for (int x = 0; x < COLUMN_NUMBER; x++) {
				try {
					this.cells[x][y].setValue(StandardCell.EMPTY_VALUE);
				} catch (InvalidCellValueException e) { }
			}
		}
		
		this.numberOfFullCells = 0;
	}
	
	public void setCell(int value, int x, int y) throws InvalidCoordinateException, InvalidCellValueException{
		Coordinate coordinate = new Coordinate(x, y);
		coordinate = validateAndRetrieveCoordinate(coordinate);
		
		int previousValue = this.cells[x][y].getValue();
		this.cells[x][y] = new StandardCell(coordinate, value); 
		
		if (previousValue == StandardCell.EMPTY_VALUE && StandardCell.EMPTY_VALUE != value) {
			// not empty input
			this.numberOfFullCells++;
		}
		else if (previousValue != StandardCell.EMPTY_VALUE && StandardCell.EMPTY_VALUE == value) {
			// value is cleared
			this.numberOfFullCells--;
		}
	}
	
	public int getCell(int x, int y) throws InvalidCoordinateException {
		Coordinate coordinate = new Coordinate(x, y);
		validateAndRetrieveCoordinate(coordinate);
		
		return this.cells[x][y].getValue();
	}
	
	public int[][] getCells() {
		int[][] valueMatrix = new int[ROW_NUMBER][COLUMN_NUMBER];
		
		for (int y = 0; y < ROW_NUMBER; y++) {
			for (int x = 0; x < ROW_NUMBER; x++) {
				valueMatrix[x][y] = cells[x][y].getValue();
			}
		}
		
		return valueMatrix;
	}
	
	public void setCells(int[][] values) throws InvalidCellValueException {
		for (int y = 0; y < COLUMN_NUMBER; y++) {
			for (int x = 0; x < ROW_NUMBER; x++) {
				cells[x][y].setValue(values[x][y]);
				if (values[x][y] != StandardCell.EMPTY_VALUE) {
					this.numberOfFullCells++;
				}
			}
		}
	}
	
	public void validate() throws SudokuValidationException{
		for (IRule rule : rules) {
			rule.check();
		}
	}
	
	private Coordinate validateAndRetrieveCoordinate(Coordinate coordinate) throws InvalidCoordinateException {
		if (coordinate != null) {
			int x = coordinate.getX();
			int y = coordinate.getY();
			
			if (!(x>=0 && x<COLUMN_NUMBER) || !(y>=0 && y<ROW_NUMBER)) {
				throw new InvalidCoordinateException("Coordinates must be between [0,8]; but actual is " + coordinate.toString());
			}
			
			return coordinate;
		}
		else {
			throw new InvalidCoordinateException("Coordinate does not exist.");
		}
	}
	
	private class LineRule implements IRule {

		@Override
		public boolean check() throws SudokuValidationException {
			int cellValue;
			int[] existingValueArray;
			Set<Coordinate> invalidCoordinates = new HashSet<Coordinate>();
			
			for (int y = 0; y < ROW_NUMBER; y++) {
				existingValueArray = StandardCell.getValueRange();
				for (int x = 0; x < COLUMN_NUMBER; x++) {
					cellValue = cells[x][y].getValue();
					if (cellValue != StandardCell.EMPTY_VALUE && existingValueArray[cellValue] == -1) {
						invalidCoordinates.add(cells[x][y].getCoordinate());
					}
					else {
						existingValueArray[cellValue] = -1;
					}
				}
			}
			
			
			for (int x = 0; x < COLUMN_NUMBER; x++) {
				existingValueArray = StandardCell.getValueRange();
				for (int y = 0; y < ROW_NUMBER; y++) {
					cellValue = cells[x][y].getValue();
					if (cellValue != StandardCell.EMPTY_VALUE) {
						if (existingValueArray[cellValue] == -1) {
							invalidCoordinates.add(cells[x][y].getCoordinate());
						}
						else {
							existingValueArray[cellValue] = -1;
						} 	
					}
				}
			}
			
			if (invalidCoordinates.size() > 0) {
				throw new SudokuValidationException("Each value must be unique on each line.", 
						new ArrayList<Coordinate>(invalidCoordinates));
			}
			
			return true;
		}
		
	}
	
	private class SquareRule implements IRule {

		@Override
		public boolean check() throws SudokuValidationException {
			List<Coordinate> invalidCoordinateList = new ArrayList<Coordinate>();
			Collection<Coordinate> invalidCoordinates;
			for (int minX = 0, maxX = 3; minX <= 6 && maxX <= 9; minX+=3,maxX+=3) {
				for (int minY = 0, maxY = 3; minY <= 6 && maxY <= 9; minY+=3,maxY+=3) {
					invalidCoordinates = checkEachSquare(minX, maxX, minY, maxY);
					invalidCoordinateList.addAll(invalidCoordinates);
				}
			}
			
			if (invalidCoordinateList.size() > 0) {
				throw new SudokuValidationException("Each 3x3 square must contain unique values.", 
						invalidCoordinateList); 
			}
			
			return true;
		}
		
		private Collection<Coordinate> checkEachSquare(int minX, int maxX, int minY, int maxY) {
			int cellValue;
			int[] existingValueArray = StandardCell.getValueRange();
			Set<Coordinate> invalidCoordinates = new HashSet<Coordinate>();
			
			for (int x = minX; x < maxX; x++) {
				for (int y = minY; y < maxY; y++) {
					cellValue = cells[x][y].getValue();
					if (cellValue != StandardCell.EMPTY_VALUE) {
						if (existingValueArray[cellValue] == -1) {
							invalidCoordinates.add(cells[x][y].getCoordinate());
						}
						else {
							existingValueArray[cellValue] = -1;
						} 	
					}
				}
			}
			
			return invalidCoordinates;
		}
		
	}
	
	public static class Memento {
		private final int[][] cellValues;
		
		public Memento(int[][] values) {
			this.cellValues = values;
		}
		
		public int[][] getCellValues() {
			return cellValues;
		}
	}

	@Override
	public Memento saveToMemento() {
		return new Memento(getCells());
	}

	@Override
	public void restoreFromMemento(Memento t) {
		try {
			this.setCells(t.getCellValues());
		} catch (InvalidCellValueException e) {		}
	}

	

}
